package app.species;

import sim.engine.*;
import sim.util.*;

import java.awt.Color;
import java.lang.reflect.Field;
import java.util.*;

import app.species.ANN.*;
import app.species.util.VisualizeBrain;
import app.species.CreatureContext;

public class Creature extends Entity {

	// ///////////////////////////////////////
	// // MUTABLE ATTRIBUTES
	// // either primitve or implements Mutable!
	// ///////////////////////////////////////

	@MutableField(probability = 0.2f, maxValue = 100, minValue = 0, defaultValue = 20)
	public Brain brain;
	
	@MutableField(probability = 0.2f,relativeImpact=0.01f, maxValue = 10000, minValue = 100)
	private int ADULT_AGE = 1000;

	private static int idCount = 0;
	public static int speciesCounter = 0;
	private static final long serialVersionUID = 5709725272768525855L;


	protected int generationCounter = 0;

	private int id, speciesNum;
	/**
	 * positiv, if asexually mated, negativ if bisexually mated, zero if none
	 */
	protected int justMated;
	private int movingFoodEaten, stationaryFoodEaten;
	private int sexualReproductions, asexualReproductions;
	protected boolean isBestMovingFoodEater, isBestStationaryFoodEater;

	private boolean visualizeBrain;

	public Creature(MutableDouble2D initPos, MutableDouble2D initHead, Brain brain) {

		super(initPos, initHead);

		this.id = Creature.idCount++;
		this.movingFoodEaten = 0;
		this.stationaryFoodEaten = 0;
		this.asexualReproductions = 0;
		this.sexualReproductions = 0;
		this.justMated = 0;

		this.setDiameter(CreatureContext.CREATURE_DEFAULT_DIAMETER);
		this.setSenseDistance(CreatureContext.DEFAULT_SENSE_DIST);
		this.setSpeed(CreatureContext.DEFAULT_SPEED);
		this.setLifeSpan(CreatureContext.DEFAULT_LIFE_SPAN);
		this.setDrawSensor(CreatureContext.CREATURE_SHOW_SENSOR);

		this.resetColor();

		// WILL CHANGE!!!!!!
		this.setEnergy(2500);

		this.brain = brain;

		this.setDrawHeading(true);

		this.isBestMovingFoodEater = false;
		this.isBestStationaryFoodEater = false;

	}

	public Creature() {
		super();
	}

	public static void setDefaults(double diameter, double speed, double senseDist, int lifeSpan) {

		CreatureContext.CREATURE_DEFAULT_DIAMETER = diameter;
		CreatureContext.DEFAULT_SPEED = speed;
		CreatureContext.DEFAULT_SENSE_DIST = senseDist;
		CreatureContext.DEFAULT_LIFE_SPAN = lifeSpan;

	}

	/*
	 * simply updates the agent's position based off of its heading and current
	 * position
	 */
	public void step(SimState state) {

		SpeciesSim sim = (SpeciesSim) state;

		// kill the creature if it has no energy or it dies of old age
		if (this.getEnergy() <= 0 || this.getAge() > this.getLifeSpan()) {
			sim.removeEntityFromSim(this, sim.creatures);
			return;
		}
		
		////////////////////////////////////////
		//GET SMALLER EQUAL AND BIGGER CREATURES
		////////////////////////////////////////
//		Bag nearbyEntities = sim.creatures.getObjectsWithinDistance(new Double2D(this.position), this.getSenseDistance());
//		Bag smallEntities = new Bag(nearbyEntities.size()/3);
//		Bag sameEntities  = new Bag(nearbyEntities.size()/3);
//		Bag bigEntities = new Bag(nearbyEntities.size()/3);
//		
//		for(int i = 0; i < nearbyEntities.size();i++){
//			Creature cTmp = (Creature) nearbyEntities.get(i);
//			double sizeRation = cTmp.getRadius()/this.getRadius();
//			if(sizeRation < 0.3) continue;
//			else if(sizeRation < 0.8) smallEntities.add(cTmp);
//			else if(sizeRation < 1.3) sameEntities.add(cTmp);
//			else if(sizeRation < 3) bigEntities.add(cTmp);			
//		}
		
		
		// this just indicates the time, that creature will be highlighted
		// (default for 10 steps)
		if (this.justMated > 0)
			this.justMated--;
		if (this.justMated < 0)
			this.justMated++;

		// decrease the energy based on speed
		float energyNeed = (brain.getSize()-3) * 0.05f;
		energyNeed += (getAcceleration()) * (getAcceleration()) * 6 + this.getDiameter() * 0.008;
		this.addEnergy(-(int) energyNeed);

		this.incrementAge();

		double[] inp = this.inputToBrain(sim, brain.getMaxInputID());
		double[] outp = this.brain.activate(inp);
//		for(int i =0;i< outp.length;i++)System.out.println(outp[i]);
		// if (outp[0] > 0)
		// System.out.println("1: " + outp[0]);
		// if (outp[1] > 0)
		// System.out.println("2: " + outp[1]);
		// if(outp[2] > 0) System.out.println("3: " +outp[2]);
		this.heading.rotate((outp[0]-outp[1]) * CreatureContext.SHARP_ANGLE_TURN);
		// this.heading.rotate(-outp[1]*Creature.SHARP_ANGLE_TURN);
		// this.heading.rotate(outp[2] * Creature.WIDE_ANGLE_TURN);
		// if(outp[2] == 1.0) this.heading.rotate(-Creature.WIDE_ANGLE_TURN);
		// if(outp[3] == 1.0) this.heading.rotate(Creature.WIDE_ANGLE_TURN);

		// output 2 increase (>0.5) or decrease (<0.5) speed
		this.setAcceleration(outp[2]-outp[3]);

		// else if(outp[2] < 1) this.addSpeed(-outp[2]);
		// else this.addSpeed(outp[2]);

		// if(this.getSpeed() < 2) this.setSpeed(2);
		// if(outp[3] == 1.0) this.addSpeed(Creature.DELTA_SPEED);

		this.interactFood(sim);
		if(getAge() > ADULT_AGE){
		this.mateWithNeighbors(sim);
		this.asexualReproduction(sim);
		}

		// updates the heading of the entity if it is hitting a wall
		this.updateHeadingHitWall(sim.fieldWidth, sim.fieldHeight);

		/*
		 * update the position in the Continuous2D container also, update the
		 * position within the agent
		 */
		this.updatePosition();
		sim.creatures.setObjectLocation(this, new Double2D(this.position));
		
		if(visualizeBrain){
			visualizeBrain = false;
			visualizeBrain(sim);
		}

	}

	private void visualizeBrain(SpeciesSim sim) {
		
		VisualizeBrain vis = new VisualizeBrain(this.brain,sim);	
	}

	// returns a
	private double[] inputToBrain(SpeciesSim sim, int maxID) {

		// if (CreatureContext.inputToBrainType.equals("EXPLICIT")) {
		return this.explicitInputToBrain(sim, maxID);
		// } else if (CreatureContext.inputToBrainType.equals("IMPLICIT")) {
		// return this.implicitInputToBrain(sim);
		// }

		// return null;

	}

	private double[] explicitInputToBrain(SpeciesSim sim, int maxID) {

		// TODO: LIMIT INPUT ARRAY TO maxID, because CREATURE will not use any
		// input greater than maxID!
		Bag nearbyEntities;
		double[] input = new double[5 * (CreatureContext.numVisionDivisions() + 1) + 5];
		// 6 because we got CREATURE,MOVING,PREDATOR,STATIONARY,TURRETS,BULLETS
		double[][] entityVisible = new double[5][CreatureContext.numVisionDivisions() + 1];

		nearbyEntities = sim.stationaryFood.getObjectsWithinDistance(new Double2D(this.position), this.getSenseDistance());
		this.explicitLookAtEntities(nearbyEntities, entityVisible[0]);

		if ((CreatureContext.numVisionDivisions() + 1) * 1 < maxID) {
			nearbyEntities = sim.movingFood.getObjectsWithinDistance(new Double2D(this.position), this.getSenseDistance());
			this.explicitLookAtEntities(nearbyEntities, entityVisible[1]);
		}

		if ((CreatureContext.numVisionDivisions() + 1) * 2 < maxID) {
			nearbyEntities = sim.creatures.getObjectsWithinDistance(new Double2D(this.position), this.getSenseDistance());
			this.explicitLookAtEntities(nearbyEntities, entityVisible[2]);
		}

		// LONG DISTANCE SCANNER
		if ((CreatureContext.numVisionDivisions() + 1) * 3 < maxID) {
			nearbyEntities = sim.stationaryFood.getObjectsWithinDistance(new Double2D(this.position),
					5 * this.getSenseDistance());
			this.explicitLookAtEntities(nearbyEntities, entityVisible[3]);
		}

		// LONG DISTANCE SCANNER

		if ((CreatureContext.numVisionDivisions() + 1) * 4 < maxID) {
			nearbyEntities = sim.movingFood.getObjectsWithinDistance(new Double2D(this.position),
					5 * this.getSenseDistance());
			this.explicitLookAtEntities(nearbyEntities, entityVisible[4]);
		}
		// nearbyEntities = sim.predators.getObjectsWithinDistance(new
		// Double2D(this.position), this.senseDistance());
		// this.explicitLookAtEntities(nearbyEntities, entityVisible[2]);

		// nearbyEntities = sim.turrets.getObjectsWithinDistance(new
		// Double2D(this.position), this.senseDistance());
		// this.explicitLookAtEntities(nearbyEntities, entityVisible[4]);
		//
		// nearbyEntities = sim.bullets.getObjectsWithinDistance(new
		// Double2D(this.position), this.senseDistance());
		// this.explicitLookAtEntities(nearbyEntities, entityVisible[5]);

		// parse booleans[][] to numbers[] (-5 for false, 5 for true)
		// TODO: can we get better input than just -5 or +5 ? Maybe continues
		// doubles?
		for (int i = 0; i < entityVisible.length; i++) {
			for (int j = 0; j < entityVisible[i].length; j++) {
				// normalize Range: [0,senseDistance] TO [-0,10]
				input[(i * entityVisible[i].length) + j] = (10 * (entityVisible[i][j])) / getSenseDistance() - 0;
			}
		}
		// give input for energy, speed and age
		input[input.length - 1] = this.getEnergy() / 1000.0;
		input[input.length - 2] = this.getSpeed() / 5.0;
		input[input.length - 3] = 5.0 * this.getAge() / getLifeSpan();
		input[input.length - 4] = 10.0 * (this.position.x / (double) sim.fieldWidth - 0.5);
		input[input.length - 5] = 10.0 * (this.position.y / (double) sim.fieldHeight - 0.5);

		return input;

	}

	private void explicitLookAtEntities(Bag nearbyEntities, double[] isVisible) {

		MutableDouble2D vectorToEntity = new MutableDouble2D();
		Entity entity;
		double angle, perpDot, dist;
		double angleDivisions = Math.PI / CreatureContext.numVisionDivisions();

		for (int i = 0; i < nearbyEntities.numObjs; i++) {

			entity = (Entity) nearbyEntities.objs[i];

			if (!entity.isAlive())
				continue;

			vectorToEntity.subtract(entity.position, this.position);
			dist = vectorToEntity.length();

			if (dist > 0.0 && dist - entity.getRadius() <= this.getSenseDistance()) {

				vectorToEntity.normalize();

				angle = this.heading.dot(vectorToEntity);
				double visibleValue = getSenseDistance() - dist;

				// check to see if the entity is in the front
				if (angle > 0.0) {

					angle = Math.acos(angle);

					perpDot = this.heading.perpDot(vectorToEntity);

					if (perpDot > 0.0) {
						angle = (Math.PI / 2.0) - angle;
					} else {
						angle = (Math.PI / 2.0) + angle;
					}

					for (int j = 0; j < isVisible.length - 1; j++) {

						if (angle <= angleDivisions * (j + 1)) {
							isVisible[j] = Math.max(isVisible[j], visibleValue);
							break;
						}

					}

					// the entity can see behind it also
				} else {

					isVisible[isVisible.length - 1] = visibleValue;

				}

			}

		}

	}

	// eat nearby food
	private void interactFood(SpeciesSim sim) {

		Bag nearbyEntities = sim.movingFood.getObjectsWithinDistance(new Double2D(this.position), this.getSenseDistance());
		MovingFood movingFood;
		StationaryFood stationaryFood;

		for (int i = 0; i < nearbyEntities.numObjs; i++) {

			movingFood = (MovingFood) nearbyEntities.objs[i];

			if (!movingFood.isAlive())
				continue;

			// check to see if the creature is touching the food. if it is, then
			// eat the food and continue to next one
			if (this.isTouching(movingFood)) {

				// get the energy from the food
				this.addEnergy(movingFood.getEnergy());
				this.movingFoodEaten++;
				sim.removeEntityFromSim(movingFood, sim.movingFood);

				continue;
			}

		}

		nearbyEntities = sim.stationaryFood.getObjectsWithinDistance(new Double2D(this.position), this.getSenseDistance());

		for (int i = 0; i < nearbyEntities.numObjs; i++) {

			stationaryFood = (StationaryFood) nearbyEntities.objs[i];

			if (!stationaryFood.isAlive())
				continue;

			// check to see if the creature is touching the food. if it is, then
			// eat the food and continue to next one
			if (this.isTouching(stationaryFood)) {

				// get the energy from the food
				this.addEnergy(stationaryFood.getEnergy());
				this.stationaryFoodEaten++;
				sim.removeEntityFromSim(stationaryFood, sim.stationaryFood);

				continue;
			}

		}

	}

	private void mateWithNeighbors(SpeciesSim sim) {

		Bag nearbyEntities = sim.creatures.getObjectsWithinDistance(new Double2D(this.position), this.getSenseDistance());
		Creature creature;

		for (int i = 0; i < nearbyEntities.numObjs; i++) {

			// this creature does not have enough energy to mate, so it dies
			if (this.getEnergy() < CreatureContext.MATING_ENERGY_THRESHOLD)
				break;

			creature = (Creature) nearbyEntities.objs[i];

			if (!creature.isAlive() || creature.getEnergy() < CreatureContext.MATING_ENERGY_THRESHOLD
					|| this == creature)
				continue;

			/*
			 * check to see if the creature is touching the creature and the
			 * energy thresholds are high enough so they can mate
			 */
			if (this.isTouching(creature)) {

				// check to see if the user specified any restrictions on mating
				if (CreatureContext.matingRestriction.equals("COMPATIBLE")) {
					if (!this.brain.isCompatible(creature.brain))
						continue;
				} else if (CreatureContext.matingRestriction.equals("SAME_SPECIES")) {
					if (this.speciesNum != creature.speciesNum)
						continue;
				}

				Creature child = Creature.mateCreatures(this, creature, sim);

				this.sexualReproductions++;
				creature.sexualReproductions++;

				this.justMated = -CreatureContext.SHOW_MATING_ALERT_TIME;
				creature.justMated = -CreatureContext.SHOW_MATING_ALERT_TIME;
				child.justMated = -CreatureContext.SHOW_MATING_ALERT_TIME;

				// update the energy
				this.addEnergy(-CreatureContext.BABY_STARTING_ENERGY / 2);
				creature.addEnergy(-CreatureContext.BABY_STARTING_ENERGY / 2);

			}

		}

	}

	private void asexualReproduction(SpeciesSim sim) {

		while (this.getEnergy() >= CreatureContext.ASEXUAL_MATE_THRESHOLD) {

			Creature child = Creature.mateCreatures(this, this, sim);

			this.asexualReproductions++;
			this.justMated = CreatureContext.SHOW_MATING_ALERT_TIME;
			child.justMated = CreatureContext.SHOW_MATING_ALERT_TIME;
			;

			this.addEnergy(-CreatureContext.BABY_STARTING_ENERGY);

		}

	}

	// Cache mutable fields, because they will not (yet) change during runtime
	private final static Field[] mutableFields;
	// static block will be executed at initialization time
	static {
		ArrayList<Field> mutFields = new ArrayList<Field>(10);
		Class<?> start = Creature.class;
		while (start.getSuperclass() != null) {
			Field[] entityFields = start.getDeclaredFields();

			for (int i = 0; i < entityFields.length; i++) {
				MutableField inherited = entityFields[i].getAnnotation(MutableField.class);
				if (inherited != null) {
					mutFields.add(entityFields[i]);
				}
			}
			start = start.getSuperclass();
		}

		mutableFields = (Field[]) mutFields.toArray(new Field[0]);
		System.out.println("Mutable Fields: ");
		for (int i = 0; i < mutableFields.length; i++) {
			System.out.println(mutableFields[i].getName() + ": " + mutableFields[i].getAnnotation(MutableField.class));
		}
	}

	/*
	 * takes in two creatures and mates them
	 */
	private static Creature mateCreatures(Creature parent1, Creature parent2, SpeciesSim sim) {

		Creature childCreature;
		MutableDouble2D childPosition, childHeading;

		// obtain an initial, random heading
		childHeading = new MutableDouble2D(sim.random.nextDouble(), sim.random.nextDouble());

		if (sim.random.nextBoolean())
			childHeading.x *= -1.0;
		if (sim.random.nextBoolean())
			childHeading.y *= -1.0;

		// normalize heading
		if (childHeading.lengthSq() > 0.0)
			childHeading.normalize();

		// obtain the position
		childPosition = new MutableDouble2D();

		childPosition.add(parent1.position, parent2.position);
		childPosition.multiplyIn(0.5);

		childCreature = new Creature(childPosition, childHeading, null);// ,
																		// parent1.brain.merge(parent2.brain));
		childCreature.generationCounter = Math.max(parent1.getGeneration(), parent2.getGeneration()) + 1;
		childCreature.resetColor();

		// inherit attributs
		for (int i = 0; i < mutableFields.length; i++) {
			// System.out.println(fields[i].getName());
			MutableField inherited = mutableFields[i].getAnnotation(MutableField.class);
			// System.out.println(fields[i].getName() + " is modfiable");
			try {
				Object p1 = mutableFields[i].get(parent1);
				Object p2 = mutableFields[i].get(parent2);

				// TODO: nicer way to handle primitives ??
				// TODO: Implement FLOAT,LONG,BOOLEAN?
				if (p1 instanceof Number) {
					double dp1 = ((Number) p1).doubleValue();
					double dp2 = ((Number) p2).doubleValue();

					// merge
					double tmp = dp1 * 0.5 + dp2 * 0.5;

					// mutate
					if (sim.random.nextFloat() < inherited.probability()) {
						tmp += (inherited.maxValue() - inherited.minValue()) * inherited.relativeImpact()
								* sim.random.nextGaussian();
					}
					if (tmp < inherited.minValue())
						tmp = inherited.minValue();
					else if (tmp > inherited.maxValue())
						tmp = inherited.maxValue();
					if (p1 instanceof Integer)
						mutableFields[i].set(childCreature, (int) tmp);
					if (p1 instanceof Double)
						mutableFields[i].set(childCreature, (double) tmp);
					if (p1 instanceof Float)
						mutableFields[i].set(childCreature, (float) tmp);
					// if(dp1 != tmp)
					// System.out.println("Field mutated: " +
					// fields[i].getName() + " old: "+ dp1 + " new: " + tmp);

				}
				if (p1 instanceof Mutable) {

					Mutable mp1 = (Mutable) p1;
					Mutable mp2 = (Mutable) p2;
					Mutable tmp = mp1.merge(mp1, mp2, sim);
					tmp = tmp.mutate(inherited.probability(), inherited.relativeImpact(), sim);
					mutableFields[i].set(childCreature, tmp);
				}

			} catch (Exception e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}

		childCreature.setEnergy(CreatureContext.BABY_STARTING_ENERGY);
		sim.creatures.setObjectLocation(childCreature, new Double2D(childPosition));

		// THIS ADDS THE CREATURE TO THE SIMULATION!
		childCreature.stoppable = sim.schedule.scheduleRepeating(childCreature);

		return childCreature;

	}

	public Color speciesColor() {
		return CreatureContext.speciesColors[this.speciesNum % CreatureContext.speciesColors.length];
	}

	public int getBrainSize() {
		return brain.getSize();
	}

	public int getSpeciesNum() {
		return this.speciesNum;
	}

	public int getGeneration() {
		return this.generationCounter;
	}

	public float getMinSpeed() {
		return this.minSpeed;
	}

	public float getMaxSpeed() {
		return this.maxSpeed;
	}

	public void setSpeciesNum(int n) {
		this.speciesNum = n;
	}

	// changes the speed of the creature based on the value given
	private void addSpeed(double delta) {

		this.setSpeed(this.getSpeed() + delta);

		if (this.getSpeed() < 0.0)
			this.setSpeed(0.0);
		// if(this.getSpeed() > Creature.DEFAULT_SPEED)
		// this.setSpeed(Creature.DEFAULT_SPEED);

	}

	public int getAsexualReproductions() {
		return this.asexualReproductions;
	}

	public int getSexualReproductions() {
		return this.sexualReproductions;
	}

	public int getStationaryFoodEaten() {
		return this.stationaryFoodEaten;
	}

	public int getMovingFoodEaten() {
		return this.movingFoodEaten;
	}

	public double[] getInputToBrain() {
		return this.inputToBrain(CreatureContext.sSim,this.brain.getMaxInputID());
	}

	public boolean getVisualizeBrain() {
		return visualizeBrain;
	}

	public void setVisualizeBrain(boolean b) {
			visualizeBrain = b;
	}

	public boolean getPrintBrain() {
		return false;
	}

	public void setPrintBrain(boolean b) {

		if (b) {
			System.out.println(this.brain.toString());
		}

	}

	public int getID() {
		return this.id;
	}

}
